Skip to content

fix(pair): emit empirically correct companion_platform_id for Android#595

Merged
jlucaso1 merged 1 commit intomainfrom
fix-companion-platform-id-mobile-letters
Apr 26, 2026
Merged

fix(pair): emit empirically correct companion_platform_id for Android#595
jlucaso1 merged 1 commit intomainfrom
fix-companion-platform-id-mobile-letters

Conversation

@jlucaso1
Copy link
Copy Markdown
Collaborator

@jlucaso1 jlucaso1 commented Apr 26, 2026

Summary

  • Add AndroidPhone / AndroidTablet / AndroidAmbiguous variants to CompanionWebClientType, emitting the single-byte ASCII letters 'e', 'd', 'f' (matching the official WhatsApp Android client).
  • Map wa::DeviceProps::PlatformType::AndroidPhone/Tablet/Ambiguous to those variants in companion_web_client_type_for_platform (instead of falling back to OtherWebClient).
  • Emit "Android (<os>)" as companion_platform_display for those variants (instead of the "Chrome (<os>)" fallback).
  • Drop the (empirically incorrect) "server validates display as <Browser> (<OS>) with browser ∈ {Chrome, Edge, Firefox, IE, Opera, Safari}" claim from docs and tests. The server only validates length 1..=100 and accepts arbitrary content.

Why

The server accepts 23 single-byte ids in companion_platform_id: digits 0..9 (WAWeb's CompanionWebClientType) and letters a..m (native clients). Of the 13 letters, three map to confirmed Android variants based on official-client RE:

Wire DeviceProps PlatformType
d AndroidTablet
e AndroidPhone
f AndroidAmbiguous

Previously, callers setting platform_type = AndroidPhone got '9' (OtherWebClient) + "Chrome (Android)". Both passed server validation by luck, but neither matches what the real Android client emits. This PR aligns with the empirical wire format.

The remaining 10 letters (a, b, c, g..m) are accepted by the server but no client has been RE'd to confirm which platform each represents. Adding speculative variants risks mislabelling the device on the primary side, so they are intentionally omitted. Future PRs can add iOS / Mac / Wear OS / AR / Quest / smartglasses letters as binary RE or live captures confirm them.

Behaviour changes

When a caller sets device_props.platform_type to an Android variant:

                   before                       after
AndroidPhone       "9" + "Chrome (Android)"     "e" + "Android (<os>)"
AndroidTablet      "9" + "Chrome (Android)"     "d" + "Android (<os>)"
AndroidAmbiguous   "9" + "Chrome (Android)"     "f" + "Android (<os>)"

For all other variants (web browsers, Desktop, UWP, iOS, AR/VR, etc.), behaviour is unchanged.

Breaking changes (intentional, pre-1.0)

  • CompanionWebClientType::code() -> i32 is replaced by wire_byte() -> u8. The new method returns the raw ASCII byte (b'1', b'e', ...) rather than an integer encoding. Display now formats the byte as a char.
  • CompanionWebClientType is no longer #[repr(i32)] with explicit discriminants. The wire mapping is centralised in wire_byte().

Add AndroidPhone/AndroidTablet/AndroidAmbiguous variants emitting the
single-byte ASCII letters 'e', 'd', 'f' that the official WhatsApp
Android client uses for companion_platform_id during pair-code/QR
companion registration.

The previous fallback (OtherWebClient = '9' + display "Chrome (Android)")
passed server validation but was idiomatic for web clients only. The
server's accept-list for companion_platform_id is 23 single-byte ids
('0'..'9' for web, 'a'..'m' for native); the three Android letters are
the only mobile mappings observed empirically so far.

Behaviour changes when the caller has set
device_props.platform_type to one of the Android variants:

  AndroidPhone     '9' + "Chrome (Android)" -> 'e' + "Android (<os>)"
  AndroidTablet    '9' + "Chrome (Android)" -> 'd' + "Android (<os>)"
  AndroidAmbiguous '9' + "Chrome (Android)" -> 'f' + "Android (<os>)"

iOS, Wear OS, AR/VR and Cloud API platforms still fall back to
OtherWebClient ('9') because no client has been reverse-engineered yet
to confirm which letter (a, b, c, g..k) maps to each of them.

Drops the (incorrect) "server validates display strictly as
Browser (OS) with Browser in {Chrome, Edge, Firefox, IE, Opera, Safari}"
claim from docs and tests. Empirically the server only validates the
display field length (1..=100) and accepts arbitrary content.

Breaking changes (intentional, pre-1.0):
  - CompanionWebClientType::code() -> i32 replaced by wire_byte() -> u8
  - CompanionWebClientType is no longer #[repr(i32)] with discriminants
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

This pull request refactors the CompanionWebClientType enum encoding from i32 discriminant-based wire integers to single-byte ASCII representation via wire_byte(). It introduces three new Android/mobile device variants (AndroidTablet, AndroidPhone, AndroidAmbiguous) mapped to letter identifiers (d, e, f), updates companion platform mapping logic to distinguish Android types, and modifies display formatting to emit Android (<OS>) for mobile variants. Tests are updated throughout to validate wire-byte encoding and new Android variant coverage.

Changes

Cohort / File(s) Summary
Enum Refactoring
wacore/src/companion_reg.rs
Removed #[repr(i32)] and explicit numeric discriminants from CompanionWebClientType; added pub const fn wire_byte(self) -> u8 replacing code(); introduced AndroidTablet, AndroidPhone, AndroidAmbitious variants; updated Display impl to render wire byte as ASCII char and refactored companion_platform_display() to emit Android (<OS>) for mobile variants.
Test Updates
wacore/src/pair.rs, wacore/src/pair_code.rs
Updated QR-data and platform tests to validate wire_byte() output instead of code() values; replaced Android variant regression expectations (from OtherWebClient collapse to dedicated variants); added wire-byte and display assertions for AndroidTablet/AndroidPhone/AndroidAmbiguous; updated IQ construction tests to expect new Android wire strings ("e"/"d"/"f") and display labels ("Android (Android)").

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

breaking-change

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main change: fixing the companion_platform_id emission for Android devices to use correct wire bytes ('d', 'e', 'f') instead of '9'.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is directly related to the changeset, providing clear motivation, behavior changes, and technical details about Android companion platform ID mappings.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-companion-platform-id-mobile-letters

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wacore/src/companion_reg.rs`:
- Around line 19-57: Replace the manual match-based wire mapping with a
WireEnum-derived enum: add #[derive(WireEnum, Debug, Clone, Copy, Default,
PartialEq, Eq, Hash)] to CompanionWebClientType and annotate every variant with
its wire value (e.g. #[wire = b'0'] on Unknown, #[wire = b'1'] on Chrome, etc.),
remove the explicit match body inside wire_byte() and make wire_byte() a thin
helper that returns the variant's wire value via the WireEnum implementation (or
remove it if redundant), and ensure you do not derive
serde::Serialize/Deserialize or add serde renames so the #[wire = ...]
attributes are the single source of truth for the protocol mapping.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e5abbc1d-fc19-4159-8fb7-b7cd261937a1

📥 Commits

Reviewing files that changed from the base of the PR and between 06809e1 and 2423e6f.

📒 Files selected for processing (3)
  • wacore/src/companion_reg.rs
  • wacore/src/pair.rs
  • wacore/src/pair_code.rs

Comment on lines 19 to 57
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum CompanionWebClientType {
// Web (digit codes from WAWebCompanionRegClientUtils.DEVICE_PLATFORM).
#[default]
Unknown = 0,
Chrome = 1,
Edge = 2,
Firefox = 3,
Ie = 4,
Opera = 5,
Safari = 6,
Electron = 7,
Uwp = 8,
OtherWebClient = 9,
Unknown,
Chrome,
Edge,
Firefox,
Ie,
Opera,
Safari,
Electron,
Uwp,
OtherWebClient,
// Mobile (letter codes from the official WhatsApp Android client).
AndroidTablet,
AndroidPhone,
AndroidAmbiguous,
}

impl CompanionWebClientType {
pub const fn code(self) -> i32 {
self as i32
/// Single-byte ASCII id placed in `<companion_platform_id>`.
pub const fn wire_byte(self) -> u8 {
match self {
Self::Unknown => b'0',
Self::Chrome => b'1',
Self::Edge => b'2',
Self::Firefox => b'3',
Self::Ie => b'4',
Self::Opera => b'5',
Self::Safari => b'6',
Self::Electron => b'7',
Self::Uwp => b'8',
Self::OtherWebClient => b'9',
Self::AndroidTablet => b'd',
Self::AndroidPhone => b'e',
Self::AndroidAmbiguous => b'f',
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Put the wire value on the enum itself.

This is protocol surface area, so the byte mapping needs to live on the variants, not in a separate manual match. Right now CompanionWebClientType and wire_byte() can drift independently, which is exactly the failure mode the repo rule is trying to prevent. Please move this enum to #[derive(WireEnum)] with per-variant #[wire = ...] annotations, and keep wire_byte() only as a thin helper if you still want that API.

As per coding guidelines, "Every protocol enum must use #[derive(WireEnum)]; the #[wire = "..."] or #[wire = NUM] attribute is the single source of truth for each variant's wire value; do not also derive serde::Serialize/Deserialize or add #[serde(rename_all)]".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/companion_reg.rs` around lines 19 - 57, Replace the manual
match-based wire mapping with a WireEnum-derived enum: add #[derive(WireEnum,
Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] to CompanionWebClientType and
annotate every variant with its wire value (e.g. #[wire = b'0'] on Unknown,
#[wire = b'1'] on Chrome, etc.), remove the explicit match body inside
wire_byte() and make wire_byte() a thin helper that returns the variant's wire
value via the WireEnum implementation (or remove it if redundant), and ensure
you do not derive serde::Serialize/Deserialize or add serde renames so the
#[wire = ...] attributes are the single source of truth for the protocol
mapping.

@jlucaso1 jlucaso1 merged commit c6603a5 into main Apr 26, 2026
12 checks passed
@jlucaso1 jlucaso1 deleted the fix-companion-platform-id-mobile-letters branch April 26, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant